unit ComponentListViewUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, Generics.Collections;

type
  IListAdapter = interface
    // function GetViewHeight(): Integer; virtual; abstract;
    function GetViewCount(): Integer;
    function GetView(const Pos: Integer; var ConvertView: TWinControl;
      AWidth: Integer; EnterEvent: TNotifyEvent): TWinControl;
    procedure ClearView(ConvertView: TWinControl);
  end;

  TComponentListView = class(TFrame)
    CotainerPanel: TPanel;
    VScrollBar: TScrollBar;
    procedure VScrollBarScroll(Sender: TObject; ScrollCode: TScrollCode;
      var ScrollPos: Integer);
    procedure CotainerPanelResize(Sender: TObject);
    procedure FrameMouseWheelDown(Sender: TObject; Shift: TShiftState;
      MousePos: TPoint; var Handled: Boolean);
    procedure FrameMouseWheelUp(Sender: TObject; Shift: TShiftState;
      MousePos: TPoint; var Handled: Boolean);
  private
    FListAdapter: IListAdapter;
    ConvertViewParent: TPanel;

    ConvertViewPool: TObjectList<TWinControl>;

    FirstTop: Integer;
    FirstIndex: Integer;
    CurScrollPos: Integer;
    // LastFoucsIndex: Integer;
    function GetFirstView(): TWinControl;
    function GetLastView(): TWinControl;
    procedure CheckScroll();
    function ShowView(I, Top: Integer): TWinControl;
    procedure ShowViewByIndex;
    procedure Scroll(ScrollPos: Integer);
    procedure ClearOutBottomView();
    function LoadView(I: Integer): TWinControl;

  protected
    procedure ViewEnter(Sender: TObject);

    procedure KeyProcc(var Msg: TMsg; var Handled: Boolean);
  public
    function GetFocusIndex(): Integer;
    procedure SetFocusByIndex(Index: Integer);
    property ListAdapter: IListAdapter read FListAdapter write FListAdapter;
    procedure NotifyDataChanged;
    procedure NotifyDataFirstChanged(AFocused: Boolean);

    constructor Create(AOwner: TComponent); override;
    destructor Destroy(); override;
  end;

implementation

{$R *.dfm}
{ TComponentListView }

procedure TComponentListView.CheckScroll;
begin
  if not Assigned(FListAdapter) or (CotainerPanel.ControlCount <= 0) then
  begin
    VScrollBar.Visible := False;
    Exit;
  end;

  if (CotainerPanel.Controls[0].Height * FListAdapter.GetViewCount) >
    CotainerPanel.Height then
  begin

    VScrollBar.Max := CotainerPanel.Controls[0].Height *
      FListAdapter.GetViewCount - CotainerPanel.Height;
    VScrollBar.Min := 0;
    CurScrollPos := FirstIndex * CotainerPanel.Controls[0].Height - FirstTop;
    VScrollBar.Position := CurScrollPos;
    VScrollBar.Visible := True;
  end
  else
    VScrollBar.Visible := False;
end;

procedure TComponentListView.CotainerPanelResize(Sender: TObject);
begin
  NotifyDataChanged;
end;

constructor TComponentListView.Create(AOwner: TComponent);
begin
  inherited;
  Application.OnMessage := KeyProcc;
  ConvertViewPool := TObjectList<TWinControl>.Create(False);
  FirstTop := 0;
  FirstIndex := 0;
  ConvertViewParent := TPanel.Create(nil);
  ConvertViewParent.Visible := False;
  ConvertViewParent.SetParentComponent(Self);
  // LastFoucsIndex := -1;

  CotainerPanel.DoubleBuffered := True;
end;

destructor TComponentListView.Destroy;
var
  I: Integer;
begin
  ConvertViewPool.Free;
  ConvertViewParent.Free;
  inherited;
end;

function TComponentListView.LoadView(I: Integer): TWinControl;
var
  ConvertView: TWinControl;
  NewView: TWinControl;
begin
  if ConvertViewPool.Count > 0 then
    ConvertView := ConvertViewPool[0]
  else
    ConvertView := nil;
  NewView := FListAdapter.GetView(I, ConvertView, CotainerPanel.Width, ViewEnter);
  if NewView = ConvertView then
  begin
    ConvertViewPool.Remove(ConvertView);
  end;
  NewView.Tag := I;
  Result := NewView;
end;

procedure TComponentListView.ClearOutBottomView();
var
  OperView: TWinControl;
begin
  OperView := GetLastView;
  if OperView <> nil then
  begin
    while (OperView.Top > CotainerPanel.Height) do
    begin
      ConvertViewPool.Add(OperView);
      FListAdapter.ClearView(OperView);
      CotainerPanel.RemoveControl(OperView);
      OperView.SetParentComponent(ConvertViewParent);
      OperView := GetLastView;
      if OperView = nil then
        Exit;
    end;
  end;
end;

procedure TComponentListView.FrameMouseWheelDown(Sender: TObject;
  Shift: TShiftState; MousePos: TPoint; var Handled: Boolean);
begin
  {
    MousePos := ScreenToClient(MousePos);
    if (MousePos.X > CotainerPanel.Left) and (MousePos.Y > CotainerPanel.Top) and
    (MousePos.X < CotainerPanel.Left + CotainerPanel.Width) and
    (MousePos.Y < CotainerPanel.Top + CotainerPanel.Height) then
    VScrollBar.Perform(WM_VSCROLL, 1, 0); }
end;

procedure TComponentListView.FrameMouseWheelUp(Sender: TObject;
  Shift: TShiftState; MousePos: TPoint; var Handled: Boolean);
begin {
    MousePos := ScreenToClient(MousePos);
    if (MousePos.X > CotainerPanel.Left) and (MousePos.Y > CotainerPanel.Top) and
    (MousePos.X < CotainerPanel.Left + CotainerPanel.Width) and
    (MousePos.Y < CotainerPanel.Top + CotainerPanel.Height) then
    VScrollBar.Perform(WM_VSCROLL, 0, 0); }
end;

procedure TComponentListView.Scroll(ScrollPos: Integer);
var
  OperView: TWinControl;
  I: Integer;
begin
  if Abs(ScrollPos - CurScrollPos) > 0 then
  begin
    if Abs(ScrollPos - CurScrollPos) >= CotainerPanel.Height then
    begin
      FirstIndex := ScrollPos div CotainerPanel.Controls[0].Height;

      FirstTop := (ScrollPos mod CotainerPanel.Controls[0].Height) * -1;

      ShowViewByIndex;
    end
    else
    begin
      for I := 0 to CotainerPanel.ControlCount - 1 do
      begin
        CotainerPanel.Controls[I].Top := CotainerPanel.Controls[I].Top -
          (ScrollPos - CurScrollPos);
      end;
      OperView := GetFirstView;
      while (OperView.Top < 0) and (Abs(OperView.Top) > OperView.Height) do
      begin
        ConvertViewPool.Add(OperView);
        FListAdapter.ClearView(OperView);
        CotainerPanel.RemoveControl(OperView);
        OperView.SetParentComponent(ConvertViewParent);
        OperView := GetFirstView;
        FirstTop := OperView.Top;
        FirstIndex := FirstIndex + 1;
      end;
      ClearOutBottomView();
      OperView := GetLastView;
      while (OperView.Top + OperView.Height) <= CotainerPanel.Height do
      begin
        if (FirstIndex + CotainerPanel.ControlCount) >
          (FListAdapter.GetViewCount - 1) then
          Break;
        ShowView(FirstIndex + CotainerPanel.ControlCount,
          OperView.Top + OperView.Height);
        OperView := GetLastView;
      end;
      OperView := GetFirstView;
      while (OperView.Top >= 0) do
      begin
        if (FirstIndex - 1) < 0 then
          Break;
        FirstIndex := FirstIndex - 1;
        FirstTop := OperView.Top - OperView.Height;
        ShowView(FirstIndex, FirstTop).TabOrder := 0;
        OperView := GetFirstView;
      end;
    end;
    CurScrollPos := ScrollPos;
  end;
end;

procedure TComponentListView.SetFocusByIndex(Index: Integer);
var
  I, MaxIndex, MinIndex: Integer;
begin
  if CotainerPanel.ControlCount <= 0 then
    Exit;
  MaxIndex := CotainerPanel.Controls[0].Tag;
  MinIndex := CotainerPanel.Controls[0].Tag;
  for I := 0 to CotainerPanel.ControlCount - 1 do
  begin
    if CotainerPanel.Controls[I].Tag = Index then
    begin
      (CotainerPanel.Controls[I] as TWinControl).SetFocus;
      Exit;
    end;
    if CotainerPanel.Controls[I].Tag < MinIndex then
      MinIndex := CotainerPanel.Controls[I].Tag;
    if CotainerPanel.Controls[I].Tag > MaxIndex then
      MaxIndex := CotainerPanel.Controls[I].Tag;
  end;
  if Index > MaxIndex then
    SetFocusByIndex(MaxIndex)
  else if Index < MinIndex then
    SetFocusByIndex(MinIndex)

end;

procedure TComponentListView.ShowViewByIndex;
var
  LastView: TWinControl;
  I: Integer;
begin
  for I := CotainerPanel.ControlCount - 1 downto 0 do
  begin
    LastView := TWinControl(CotainerPanel.Controls[I]);
    ConvertViewPool.Add(LastView);
    FListAdapter.ClearView(LastView);
    CotainerPanel.RemoveControl(LastView);
    LastView.SetParentComponent(ConvertViewParent);
  end;

  for I := FirstIndex to FListAdapter.GetViewCount - 1 do
  begin
    LastView := GetLastView;
    if (LastView <> nil) and ((LastView.Top + LastView.Height) >
      CotainerPanel.Height) then
      Break;
    if LastView = nil then
      ShowView(I, FirstTop)
    else
      ShowView(I, LastView.Top + LastView.Height);
  end;
end;

function TComponentListView.ShowView(I, Top: Integer): TWinControl;
begin
  Result := LoadView(I);
  Result.Top := Top;
  Result.SetParentComponent(CotainerPanel);
end;

function TComponentListView.GetFirstView: TWinControl;
var
  I: Integer;
begin
  Result := nil;
  for I := 0 to CotainerPanel.ControlCount - 1 do
  begin
    if (Result = nil) or (Result.Top > CotainerPanel.Controls[I].Top) then
    begin
      Result := TWinControl(CotainerPanel.Controls[I]);
    end;
  end;
end;

function TComponentListView.GetFocusIndex: Integer;
var
  I: Integer;
begin
  Result := -1;
  for I := 0 to CotainerPanel.ControlCount - 1 do
  begin
    if (CotainerPanel.Controls[I] as TWinControl).Focused then
    begin
      Result := CotainerPanel.Controls[I].Tag;
      Exit;
    end;
  end;
end;

function TComponentListView.GetLastView: TWinControl;
var
  I: Integer;
begin
  Result := nil;
  for I := 0 to CotainerPanel.ControlCount - 1 do
  begin
    if (Result = nil) or (Result.Top < CotainerPanel.Controls[I].Top) then
    begin
      Result := TWinControl(CotainerPanel.Controls[I]);
    end;
  end;
end;

procedure TComponentListView.KeyProcc(var Msg: TMsg; var Handled: Boolean);
var
  CurFocusIndex: Integer;
begin
  if (FListAdapter <> nil) and (Msg.message = WM_KEYDOWN) and
    VScrollBar.Visible and
    (FListAdapter.GetViewCount > CotainerPanel.ControlCount) then
  begin

    if Msg.wParam in [VK_UP, VK_RIGHT] then
    begin
      CurFocusIndex := GetFocusIndex;
      if (CurFocusIndex = 0) then
      begin
        VScrollBar.Position := VScrollBar.Max;
        Scroll(VScrollBar.Position);
        GetLastView.SetFocus;
      end;
    end
    else if Msg.wParam in [VK_DOWN, VK_LEFT] then
    begin
      CurFocusIndex := GetFocusIndex;
      if CurFocusIndex >= (FListAdapter.GetViewCount - 1) then
      begin
        VScrollBar.Position := 0;
        Scroll(VScrollBar.Position);
        GetFirstView.SetFocus;
      end;
    end;

  end;
end;

procedure TComponentListView.NotifyDataChanged;
var
  OldFocusIndex, I: Integer;
begin
  if not Assigned(FListAdapter) then
    Exit;

  if (FListAdapter.GetViewCount > 0) and
    (FListAdapter.GetViewCount - 1 < FirstIndex) then
  begin
    OldFocusIndex := GetFocusIndex;
    FirstIndex := FListAdapter.GetViewCount - 1;
  end
  else
    OldFocusIndex := -1;

  ShowViewByIndex;
  CheckScroll();
  if VScrollBar.Visible then
  begin
    with GetLastView do
    begin
      if (Top + Height < CotainerPanel.Height) then
      begin
        VScrollBar.Position := VScrollBar.Max;
        Scroll(VScrollBar.Position);
      end;
    end;
  end
  else if (FListAdapter.GetViewCount > 0) and (GetFirstView.Top < 0) then
  begin
    for I := 0 to CotainerPanel.ControlCount - 1 do
    begin
      CotainerPanel.Controls[I].Top := CotainerPanel.Controls[I].Top - FirstTop;
    end;
    FirstTop := 0;
  end;

  if OldFocusIndex >= 0 then
  begin
    SetFocusByIndex(OldFocusIndex);
  end;

end;

procedure TComponentListView.NotifyDataFirstChanged(AFocused: Boolean);
var
  I, MoveHeight: Integer;
  TopView, NewView: TWinControl;
begin
  NewView := LoadView(0);
  TopView := GetFirstView();

  if (TopView = nil) or ((TopView.Tag = 0) and (Abs(TopView.Top) < TopView.Height)) then
  begin
    if TopView <> nil then
    begin
      //TopView.Focused
      MoveHeight:= Abs(TopView.Top) + TopView.Height;
      for I := 0 to CotainerPanel.ControlCount - 1 do
      begin
        with CotainerPanel.Controls[I] do
        begin
          Top := Top + MoveHeight;
          Tag := Tag + 1;
        end;
      end;
    end;
    NewView.Top := 0;
    NewView.SetParentComponent(CotainerPanel);
    if AFocused then
      NewView.SetFocus;
    NewView.TabOrder := 0;
    ClearOutBottomView();
    CheckScroll();
  end;
end;

procedure TComponentListView.ViewEnter(Sender: TObject);
begin

  if VScrollBar.Visible then
  begin
    with Sender as TWinControl do
    begin
      if Top < 0 then
      begin
        VScrollBar.Position := VScrollBar.Position + Top;
        Scroll(VScrollBar.Position);
      end
      else if (Top + Height) > CotainerPanel.Height then
      begin
        VScrollBar.Position := VScrollBar.Position +
          ((Top + Height) - CotainerPanel.Height);
        Scroll(VScrollBar.Position);
      end;
    end;
  end;
end;

procedure TComponentListView.VScrollBarScroll(Sender: TObject;
  ScrollCode: TScrollCode; var ScrollPos: Integer);
begin
  Scroll(ScrollPos);
end;

end.
